/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
 * Copyright (C) 2013 Red Hat, Inc.
 */

/**
 * SECTION:nmt-editor-grid
 * @short_description: Grid widget for #NmtEditorPages
 *
 * #NmtEditorGrid is the layout grid used by #NmtEditorPages. It
 * consists of a number of rows, each containing either a single
 * widget that spans the entire width of the row, or else containing a
 * label, a widget, and an optional extra widget.
 *
 * Each row of the grid can take up multiple on-screen rows, if
 * its main widget is multiple rows high. The label and extra widgets
 * will be top-aligned if the row is taller than they are.
 *
 * The #NmtEditorGrids in a form behave as though they are all in a
 * "size group" together; they will all use the same column widths,
 * which will be wide enough for the widest labels/widgets in any of
 * the grids. #NmtEditorGrid is also specially aware of #NmtNewtSection,
 * and grids inside sections will automatically take the size of the
 * section border into account as well.
 */

#include "libnm-client-aux-extern/nm-default-client.h"

#include "nmt-editor-grid.h"

G_DEFINE_TYPE(NmtEditorGrid, nmt_editor_grid, NMT_TYPE_NEWT_CONTAINER)

#define NMT_EDITOR_GRID_GET_PRIVATE(o) \
    (G_TYPE_INSTANCE_GET_PRIVATE((o), NMT_TYPE_EDITOR_GRID, NmtEditorGridPrivate))

typedef struct {
    GArray *rows;
    int    *row_heights;
    int     indent;
} NmtEditorGridPrivate;

typedef struct {
    NmtNewtWidget        *label;
    NmtNewtWidget        *widget;
    NmtNewtWidget        *extra;
    NmtEditorGridRowFlags flags;
} NmtEditorGridRow;

typedef struct {
    int col_widths[3];
} NmtEditorGridFormState;

/**
 * nmt_editor_grid_new:
 *
 * Creates a new #NmtEditorGrid
 *
 * Returns: a new #NmtEditorGrid
 */
NmtNewtWidget *
nmt_editor_grid_new(void)
{
    return g_object_new(NMT_TYPE_EDITOR_GRID, NULL);
}

static void
nmt_editor_grid_init(NmtEditorGrid *grid)
{
    NmtEditorGridPrivate *priv = NMT_EDITOR_GRID_GET_PRIVATE(grid);

    priv->rows = g_array_new(FALSE, TRUE, sizeof(NmtEditorGridRow));
}

static void
nmt_editor_grid_finalize(GObject *object)
{
    NmtEditorGridPrivate *priv = NMT_EDITOR_GRID_GET_PRIVATE(object);

    g_array_unref(priv->rows);
    nm_clear_g_free(&priv->row_heights);

    G_OBJECT_CLASS(nmt_editor_grid_parent_class)->finalize(object);
}

/**
 * nmt_editor_grid_append:
 * @grid: the #NmtEditorGrid
 * @label: (nullable): the label text for @widget, or %NULL
 * @widget: (nullable): the (main) widget
 * @extra: (nullable): optional extra widget
 *
 * Adds a row to @grid.
 *
 * If @label and @widget are both non-%NULL, this will add a three-column row,
 * containing a right-aligned #NmtNewtLabel in the first column, @widget in the
 * second column, and @extra (if non-%NULL) in the third column.
 *
 * If either @label or @widget is %NULL, then the other column will expand into
 * it.
 *
 * See also nmt_editor_grid_set_row_flags().
 */
void
nmt_editor_grid_append(NmtEditorGrid *grid,
                       const char    *label,
                       NmtNewtWidget *widget,
                       NmtNewtWidget *extra)
{
    NmtEditorGridPrivate  *priv         = NMT_EDITOR_GRID_GET_PRIVATE(grid);
    NmtNewtContainerClass *parent_class = NMT_NEWT_CONTAINER_CLASS(nmt_editor_grid_parent_class);
    NmtNewtContainer      *container    = NMT_NEWT_CONTAINER(grid);
    NmtEditorGridRow       row;

    g_return_if_fail(label != NULL || widget != NULL);

    memset(&row, 0, sizeof(row));

    if (label && !widget) {
        widget = nmt_newt_label_new(label);
        label  = NULL;
    }

    if (label) {
        row.label = nmt_newt_label_new(label);
        parent_class->add(container, row.label);
    }

    row.widget = widget;
    parent_class->add(container, widget);
    if (row.label) {
        g_object_bind_property(row.widget,
                               "valid",
                               row.label,
                               "highlight",
                               G_BINDING_INVERT_BOOLEAN | G_BINDING_SYNC_CREATE);
    }

    if (extra) {
        row.extra = extra;
        parent_class->add(container, extra);
    }

    g_array_append_val(priv->rows, row);
}

static int
nmt_editor_grid_find_widget(NmtEditorGrid *grid, NmtNewtWidget *widget)
{
    NmtEditorGridPrivate *priv = NMT_EDITOR_GRID_GET_PRIVATE(grid);
    NmtEditorGridRow     *rows = nm_g_array_first_p(priv->rows, NmtEditorGridRow);
    int                   i;

    for (i = 0; i < priv->rows->len; i++) {
        if (rows[i].label == widget || rows[i].widget == widget || rows[i].extra == widget)
            return i;
    }

    return -1;
}

/**
 * NmtEditorGridRowFlags:
 * @NMT_EDITOR_GRID_ROW_LABEL_ALIGN_LEFT: the row's label should be
 *   aligned left instead of right.
 * @NMT_EDITOR_GRID_ROW_EXTRA_ALIGN_RIGHT: the row's extra widget
 *   should be aligned right instead of left.
 *
 * Flags to alter an #NmtEditorGrid row's layout.
 */

/**
 * nmt_editor_grid_set_row_flags:
 * @grid: an #NmtEditorGrid
 * @widget: the widget whose row you want to adjust
 * @flags: the flags to set
 *
 * Sets flags to adjust the layout of @widget's row in @grid.
 */
void
nmt_editor_grid_set_row_flags(NmtEditorGrid        *grid,
                              NmtNewtWidget        *widget,
                              NmtEditorGridRowFlags flags)
{
    NmtEditorGridPrivate *priv = NMT_EDITOR_GRID_GET_PRIVATE(grid);
    NmtEditorGridRow     *rows = nm_g_array_first_p(priv->rows, NmtEditorGridRow);
    int                   i;

    i = nmt_editor_grid_find_widget(grid, widget);
    if (i != -1)
        rows[i].flags = flags;
}

static void
nmt_editor_grid_remove(NmtNewtContainer *container, NmtNewtWidget *widget)
{
    NmtEditorGrid         *grid         = NMT_EDITOR_GRID(container);
    NmtEditorGridPrivate  *priv         = NMT_EDITOR_GRID_GET_PRIVATE(grid);
    NmtNewtContainerClass *parent_class = NMT_NEWT_CONTAINER_CLASS(nmt_editor_grid_parent_class);
    NmtEditorGridRow      *rows         = nm_g_array_first_p(priv->rows, NmtEditorGridRow);
    int                    i;

    i = nmt_editor_grid_find_widget(grid, widget);
    if (i != -1) {
        if (rows[i].label)
            parent_class->remove(container, rows[i].label);
        parent_class->remove(container, rows[i].widget);
        if (rows[i].extra)
            parent_class->remove(container, rows[i].extra);

        g_array_remove_index(priv->rows, i);
        return;
    }

    // FIXME: shouldn't happen
    parent_class->remove(container, widget);
}

static newtComponent *
nmt_editor_grid_get_components(NmtNewtWidget *widget)
{
    NmtEditorGridPrivate *priv = NMT_EDITOR_GRID_GET_PRIVATE(widget);
    NmtEditorGridRow     *rows = nm_g_array_first_p(priv->rows, NmtEditorGridRow);
    newtComponent        *child_cos;
    GPtrArray            *cos;
    int                   i, c;

    cos = g_ptr_array_new();

    for (i = 0; i < priv->rows->len; i++) {
        if (!nmt_newt_widget_get_visible(rows[i].widget))
            continue;

        if (rows[i].label) {
            child_cos = nmt_newt_widget_get_components(rows[i].label);
            g_return_val_if_fail(child_cos[0] && !child_cos[1], NULL);
            g_ptr_array_add(cos, child_cos[0]);
            g_free(child_cos);
        }

        child_cos = nmt_newt_widget_get_components(rows[i].widget);
        for (c = 0; child_cos[c]; c++)
            g_ptr_array_add(cos, child_cos[c]);
        g_free(child_cos);

        if (rows[i].extra) {
            child_cos = nmt_newt_widget_get_components(rows[i].extra);
            for (c = 0; child_cos[c]; c++)
                g_ptr_array_add(cos, child_cos[c]);
            g_free(child_cos);
        }
    }

    g_ptr_array_add(cos, NULL);
    return (newtComponent *) g_ptr_array_free(cos, FALSE);
}

static NmtEditorGridFormState *
get_form_state(NmtNewtWidget *widget)
{
    NmtNewtForm            *form = nmt_newt_widget_get_form(widget);
    NmtEditorGridFormState *state;

    if (!form)
        return NULL;

    state = g_object_get_data(G_OBJECT(form), "NmtEditorGridFormState");
    if (state)
        return state;

    state = g_new0(NmtEditorGridFormState, 1);
    g_object_set_data_full(G_OBJECT(form), "NmtEditorGridFormState", state, g_free);
    return state;
}

static void
nmt_editor_grid_realize(NmtNewtWidget *widget)
{
    NmtEditorGridPrivate *priv = NMT_EDITOR_GRID_GET_PRIVATE(widget);
    NmtNewtWidget        *parent;

    NMT_NEWT_WIDGET_CLASS(nmt_editor_grid_parent_class)->realize(widget);

    /* This is a hack, but it's the simplest way to make it work... */
    priv->indent = 0;

    parent = nmt_newt_widget_get_parent(widget);
    while (parent) {
        if (NMT_IS_NEWT_SECTION(parent)) {
            priv->indent = 2;
            break;
        }
        parent = nmt_newt_widget_get_parent(parent);
    }
}

static void
nmt_editor_grid_unrealize(NmtNewtWidget *widget)
{
    NmtEditorGridFormState *state = get_form_state(widget);

    if (state)
        memset(state->col_widths, 0, sizeof(state->col_widths));

    NMT_NEWT_WIDGET_CLASS(nmt_editor_grid_parent_class)->unrealize(widget);
}

static void
nmt_editor_grid_size_request(NmtNewtWidget *widget, int *width, int *height)
{
    NmtEditorGridPrivate   *priv        = NMT_EDITOR_GRID_GET_PRIVATE(widget);
    NmtEditorGridRow       *rows        = nm_g_array_first_p(priv->rows, NmtEditorGridRow);
    NmtEditorGridFormState *state       = get_form_state(widget);
    gboolean                add_padding = FALSE;
    int                     i;

    g_free(priv->row_heights);
    priv->row_heights = g_new0(int, priv->rows->len);

    *height = 0;
    for (i = 0; i < priv->rows->len; i++) {
        int lwidth, lheight, wwidth, wheight, ewidth, eheight;

        if (!nmt_newt_widget_get_visible(rows[i].widget))
            continue;

        if (rows[i].label) {
            nmt_newt_widget_size_request(rows[i].label, &lwidth, &lheight);
            lwidth += priv->indent;
            state->col_widths[0] = MAX(state->col_widths[0], lwidth);

            nmt_newt_widget_size_request(rows[i].widget, &wwidth, &wheight);
            state->col_widths[1] = MAX(state->col_widths[1], wwidth);
            priv->row_heights[i] = wheight;

            add_padding = TRUE;
        } else {
            nmt_newt_widget_size_request(rows[i].widget, &wwidth, &wheight);
            priv->row_heights[i] = wheight;
        }

        if (rows[i].extra) {
            nmt_newt_widget_size_request(rows[i].extra, &ewidth, &eheight);
            state->col_widths[2] = MAX(state->col_widths[2], ewidth);
            priv->row_heights[i] = MAX(priv->row_heights[i], eheight);
        }

        *height += priv->row_heights[i];
    }

    *width = state->col_widths[0] + state->col_widths[1] + state->col_widths[2];
    if (add_padding)
        *width += 2;
}

static void
nmt_editor_grid_size_allocate(NmtNewtWidget *widget, int x, int y, int width, int height)
{
    NmtEditorGridPrivate   *priv  = NMT_EDITOR_GRID_GET_PRIVATE(widget);
    NmtEditorGridRow       *rows  = nm_g_array_first_p(priv->rows, NmtEditorGridRow);
    NmtEditorGridFormState *state = get_form_state(widget);
    int                     col0_width, col1_width, col2_width;
    int                     i, row;

    col0_width = state->col_widths[0] - priv->indent;
    col1_width = state->col_widths[1];
    col2_width = state->col_widths[2];

    for (i = row = 0; i < priv->rows->len; i++) {
        if (!nmt_newt_widget_get_visible(rows[i].widget))
            continue;

        if (rows[i].label) {
            int lwidth, lheight, lx;

            if (rows[i].flags & NMT_EDITOR_GRID_ROW_LABEL_ALIGN_LEFT)
                lx = x;
            else {
                nmt_newt_widget_size_request(rows[i].label, &lwidth, &lheight);
                lx = x + col0_width - lwidth;
            }

            nmt_newt_widget_size_allocate(rows[i].label,
                                          lx,
                                          y + row,
                                          col0_width,
                                          priv->row_heights[i]);

            nmt_newt_widget_size_allocate(rows[i].widget,
                                          x + col0_width + 1,
                                          y + row,
                                          col1_width,
                                          priv->row_heights[i]);
        } else {
            nmt_newt_widget_size_allocate(rows[i].widget,
                                          x,
                                          y + row,
                                          col0_width + col1_width + 1,
                                          priv->row_heights[i]);
        }

        if (rows[i].extra) {
            int wwidth, wheight, ex;

            if (rows[i].flags & NMT_EDITOR_GRID_ROW_EXTRA_ALIGN_RIGHT)
                ex = x + col0_width + col1_width + 2;
            else {
                nmt_newt_widget_size_request(rows[i].widget, &wwidth, &wheight);
                ex = x + col0_width + wwidth + 2;
            }

            nmt_newt_widget_size_allocate(rows[i].extra,
                                          ex,
                                          y + row,
                                          col2_width,
                                          priv->row_heights[i]);
        }

        row += priv->row_heights[i];
    }
}

static void
nmt_editor_grid_class_init(NmtEditorGridClass *grid_class)
{
    GObjectClass          *object_class    = G_OBJECT_CLASS(grid_class);
    NmtNewtWidgetClass    *widget_class    = NMT_NEWT_WIDGET_CLASS(grid_class);
    NmtNewtContainerClass *container_class = NMT_NEWT_CONTAINER_CLASS(grid_class);

    g_type_class_add_private(grid_class, sizeof(NmtEditorGridPrivate));

    /* virtual methods */
    object_class->finalize = nmt_editor_grid_finalize;

    widget_class->realize        = nmt_editor_grid_realize;
    widget_class->unrealize      = nmt_editor_grid_unrealize;
    widget_class->get_components = nmt_editor_grid_get_components;
    widget_class->size_request   = nmt_editor_grid_size_request;
    widget_class->size_allocate  = nmt_editor_grid_size_allocate;

    container_class->remove = nmt_editor_grid_remove;
}
